namespace ScrumSprintMonitor.Implementation
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Linq;
	using System.Threading;

	using AvailableTimeCalculator;

	using Infrastructure;
	using Infrastructure.Configuration;

	using Ninject.Core;
	using Ninject.Core.Parameters;

	#endregion

	public class Sprint : ISprint,
	                      IDisposable
	{
		#region Private Fields

		private readonly ITeamMemberAvailableTimeCalculator availableTimeCalculator;
		private readonly IConnection connection;
		private readonly IKernel kernel;
		private readonly ILogger<Sprint> logger;
		private readonly IShellLayoutConfiguration shellLayoutConfiguration;
		private readonly ITeamMemberAvailableTimeCalculator simpleAvailableTimeCalculator;
		private readonly ISprintBurndown sprintBurndown;
		private readonly ISprintConfiguration sprintConfiguration;

		private readonly SynchronizationContext syncContext;
		private readonly ITeamMemberInfoProvider teamInfoProvider;
		private readonly BindingList<ITeamMember> teamMembersList = new BindingList<ITeamMember>();
		private readonly Timer timer;

		private readonly IWorkItemInterpretationConfiguration workItemInterpretationConfiguration;
		private readonly TimeSpan workItemQueryLifetime = TimeSpan.FromMinutes(1.0);

		private TimeSpan availableTime;
		private bool disposed;
		private Exception exception;
		private DateTime? lastWorkItemsRefresh, lastWorkItemsUpdate;
		private string name;
		private bool notAllWorkItemsEstimated;
		private SprintStatus status = SprintStatus.NoDataAvailable;
		private bool teamInfoProviderSubscribed;
		private bool timeIncludesFreeBusyInformation;
		private TimeSpan totalTime;
		private IWorkItemStore workItemStore;

		#endregion

		public Sprint(IKernel kernel, IConnection connection, ISprintConfiguration sprintConfiguration, IShellLayoutConfiguration shellLayoutConfiguration,
		              IWorkItemInterpretationConfiguration workItemInterpretationConfiguration, ITeamMemberAvailableTimeCalculator availableTimeCalculator,
		              ITeamMemberFreeBusyInfo freeBusyInfo, ITeamMemberInfoProvider teamInfoProvider, ILogger<Sprint> logger)
		{
			if (kernel == null)
			{
				throw new ArgumentNullException("kernel");
			}
			if (connection == null)
			{
				throw new ArgumentNullException("connection");
			}
			if (sprintConfiguration == null)
			{
				throw new ArgumentNullException("sprintConfiguration");
			}
			if (shellLayoutConfiguration == null)
			{
				throw new ArgumentNullException("shellLayoutConfiguration");
			}
			if (availableTimeCalculator == null)
			{
				throw new ArgumentNullException("availableTimeCalculator");
			}
			if (teamInfoProvider == null)
			{
				throw new ArgumentNullException("teamInfoProvider");
			}
			if (sprintConfiguration.SprintEndDate < sprintConfiguration.SprintStartDate)
			{
				throw new ArgumentException("Sprint dates are incorrect.", "sprintConfiguration");
			}

			this.kernel = kernel;
			this.shellLayoutConfiguration = shellLayoutConfiguration;
			this.connection = connection;
			this.sprintConfiguration = sprintConfiguration;
			this.workItemInterpretationConfiguration = workItemInterpretationConfiguration;
			this.availableTimeCalculator = availableTimeCalculator;
			this.simpleAvailableTimeCalculator = new SimpleTeamMemberAvailableTimeCalculator(freeBusyInfo);
			this.teamInfoProvider = teamInfoProvider;
			this.sprintBurndown = this.kernel.Get<ISprintBurndown>(With.Parameters.ConstructorArgument("sprint", this));
			this.logger = logger;

			this.syncContext = SynchronizationContext.Current;

			if (this.syncContext != null)
			{
				this.timer = new Timer(OnTimer, null, 1000, Timeout.Infinite);
			}
		}

		~Sprint()
		{
			Dispose(false);
		}

		#region Private Methods/Properties

		private static readonly IEnumerable<IWorkItemAdapter> EmptyWorkItemsList = new IWorkItemAdapter[] {};

		private bool WorkItemQueryOutdated
		{
			get { return !LastRefresh.HasValue || (SystemTime.Now() - LastRefresh.Value) >= this.workItemQueryLifetime; }
		}

		[DebuggerStepThrough]
		private void GuardDisposed()
		{
			if (this.disposed)
			{
				throw new ObjectDisposedException("Sprint");
			}
		}

		/// <summary>
		/// This method redistributes unassigned work through all the underassigned team members.
		/// </summary>
		private static void RedistributeUnassignedWorkItems(IEnumerable<ITeamMember> globalTeamMembersList, TimeSpan availableTime)
		{
			foreach (var unassignedTeamMember in globalTeamMembersList.Where(m => m.Information.Unassigned))
			{
				string team = unassignedTeamMember.Information.Team;
				var teamMembersFromTeam = string.IsNullOrEmpty(team)
				                          	? globalTeamMembersList.Where(m => !m.Information.Unassigned)
				                          	: globalTeamMembersList.Where(m => !m.Information.Unassigned && m.Information.Team == team);
				int teamMemberCount = teamMembersFromTeam.Count();

				if (teamMemberCount != 0)
				{
					var averageAvailableTime = TimeSpan.FromTicks(availableTime.Ticks / teamMemberCount);
					double averageUnassignedWork = (unassignedTeamMember.RemainingWorkload.SprintWork / teamMemberCount);
					var dedicatedTeamMembers = teamMembersFromTeam.Where(m => m.Information.DedicatedToSprint);
					var underassignedTeamMembers =
						dedicatedTeamMembers.Where(m => m.RemainingWorkload.SprintWork + averageUnassignedWork < averageAvailableTime.TotalHours);
					int underassignedTeamMemberCount = underassignedTeamMembers.Count();

					if (underassignedTeamMemberCount <= 1)
					{
						underassignedTeamMembers = dedicatedTeamMembers;
						underassignedTeamMemberCount = dedicatedTeamMembers.Count();
					}

					double unassignedWorkloadPerTeamMember = (unassignedTeamMember.RemainingWorkload.SprintWork / underassignedTeamMemberCount);
					Parallel.ForEach(underassignedTeamMembers, teamMember => teamMember.RemainingWorkload.UnassignedSprintWork = unassignedWorkloadPerTeamMember);
					unassignedTeamMember.RemainingWorkload.PotentialSprintHours =
						dedicatedTeamMembers.AsParallel().Sum(m => m.RemainingWorkload.PotentialSprintHours - m.RemainingWorkload.SprintWork);
				}
			}
		}

		private void CalculateAvailableTimeForSprintFrom(IEnumerable<ITeamMember> teamMembers)
		{
			AvailableTime = TimeSpan.FromHours(teamMembers.Sum(m => m.RemainingWorkload.SprintHours));
			TimeIncludesFreeBusyInformation = !teamMembers.Any(m => !m.RemainingWorkload.IncludesFreeBusyInformation);
		}

		private void ProcessWorkItems()
		{
			this.logger.Info("Processing work item store...");

			try
			{
				var workItems = this.workItemStore.WorkItems;

				var firstUnestimatedWorkItem = workItems.Where(x => x.Type == WorkItemType.SprintBacklogItem).FirstOrDefault(x => double.IsNaN(x.RemainingWork));
				NotAllWorkItemsEstimated = (firstUnestimatedWorkItem != null);
				if (NotAllWorkItemsEstimated && !this.workItemInterpretationConfiguration.AllowUnestimatedWorkItems)
				{
					throw new RemainingWorkNotEstimatedException(firstUnestimatedWorkItem);
				}

				lock (TeamMembers)
				{
					ExtractTeamMembersFromWorkItems();

					UpdateTimeCalculations();

					var unassignedTeamMembers = TeamMembers.Where(m => m.Information.Unassigned);
					var unnecessaryTeamMembers = unassignedTeamMembers.Where(m => m.RemainingWorkload.SprintWork == 0.0);
					if (unnecessaryTeamMembers.Any())
					{
						foreach (var unassignedTeamMember in unnecessaryTeamMembers.ToArray())
						{
							TeamMembers.Remove(unassignedTeamMember);
						}
					}
				}

				LastUpdate = SystemTime.Now();
				Status = SprintStatus.OK;

				this.logger.Info("Processed work item store.");
			}
			catch (AggregateException ae)
			{
				this.logger.Error(ae, "Exception detected while processing sprint work items.");

				Exception = ae.InnerException;
			}
			catch (RemainingWorkNotEstimatedException rwnee)
			{
				this.logger.Error(rwnee, "Unestimated work item detected while processing sprint work items.");

				Exception = rwnee;
			}
			catch (Exception e)
			{
				this.logger.Error(e, "Exception detected while processing sprint work items.");

				Exception = e;
			}

			if (this.shellLayoutConfiguration.ShowBurndownChart)
			{
				this.sprintBurndown.BeginBuildBurndownData();
			}
		}

		private void UpdateTimeCalculations()
		{
			this.logger.Info("Updating time calculations...");

			try
			{
				lock (TeamMembers)
				{
					if (TeamMembers.Any())
					{
						if (this.availableTimeCalculator is SimpleTeamMemberAvailableTimeCalculator)
						{
							TotalTime = TimeSpan.FromHours(TeamMembers.AsParallel().Sum(m => GetTotalHoursForTeamMember(m)));
							TeamMembers.AsParallel().ForAll(CalculateAvailableTimeForTeamMember);
						}
						else
						{
							TotalTime = TimeSpan.FromHours(TeamMembers.Sum(m => GetTotalHoursForTeamMember(m)));

							foreach (var teamMember in TeamMembers)
							{
								CalculateAvailableTimeForTeamMember(teamMember);
							}
						}
					}
					if (TeamMembers.Any(m => m.Information.Unassigned))
					{
						CalculateAvailableTimeForSprintFrom(TeamMembers);
						RedistributeUnassignedWorkItems(TeamMembers, AvailableTime);
					}
					CalculateAvailableTimeForSprintFrom(TeamMembers); // Recalculate available time after capping non-dedicated team members' time
				}
			}
			catch (ConnectionException ce)
			{
				this.logger.Error(ce, "Exception detected while updating available time calculations.");

				Exception = ce;

				return;
			}

			this.logger.Info("Updated time calculations.");
		}

		private void ExtractTeamMembersFromWorkItems()
		{
			if (this.workItemStore == null)
			{
				TeamMembers.Clear();
				return;
			}

			var sprintBacklogItems = this.workItemStore.WorkItems.Where(w => w.Type == WorkItemType.SprintBacklogItem);

			switch (this.shellLayoutConfiguration.ActiveSprintMode)
			{
				case SprintMode.Planning:
					foreach (var teamMemberInfo in TeamInfoProvider.TeamMembers)
					{
						string teamMemberName = teamMemberInfo.Name;

						var info = teamMemberInfo;
						var teamMember = TeamMembers.FirstOrDefault(m => m.Information == info);
						if (teamMember == null)
						{
							teamMember = (teamMemberInfo.Unassigned
							              	? CreateUnassignedTeamMemberForTeam(teamMemberInfo.Team, sprintBacklogItems)
							              	: CreateTeamMember(teamMemberName, sprintBacklogItems));
							TeamMembers.Add(teamMember);
						}
						else
						{
							UpdateAssignedWorkItems(teamMember, sprintBacklogItems);
						}
					}
					break;
				case SprintMode.Sprinting:
				{
					var teamNamesOfUnassignedItems =
						sprintBacklogItems.AsParallel().AsUnordered().Where(x => string.IsNullOrEmpty(x.AssignedTo)).Select(x => GetNormalizedTeamName(x.Team)).Distinct
							().ToArray();
					var teamMemberNames =
						sprintBacklogItems.AsParallel().AsUnordered().Where(x => !string.IsNullOrEmpty(x.AssignedTo)).Select(x => x.AssignedTo).Distinct().ToArray();

					// Add any team members which do not exist in the current list
					foreach (string teamMemberName in teamMemberNames)
					{
						string localTeamMemberName = teamMemberName;
						if (!TeamMembers.Any(x => x.Information.Name == localTeamMemberName))
						{
							TeamMembers.Add(CreateTeamMember(teamMemberName, null));
						}
					}

					// Add any unassigned team members which do not exist in the current list
					foreach (string teamNameForUnassignedItems in teamNamesOfUnassignedItems)
					{
						string teamName = teamNameForUnassignedItems;
						if (!TeamMembers.Any(x => x.Information.Unassigned && x.Information.Team == teamName))
						{
							TeamMembers.Add(CreateUnassignedTeamMemberForTeam(teamName, null));
						}
					}

					// Remove any team members in the current list that no longer belong to the sprint
					List<ITeamMember> invalidTeamMembersList = null;
					foreach (var teamMember in TeamMembers)
					{
						bool invalid;
						var member = teamMember;
						if (teamMember.Information.Unassigned)
						{
							invalid = (!teamNamesOfUnassignedItems.Any(x => x == member.Information.Team));
						}
						else
						{
							invalid = (!teamMemberNames.Any(x => x == member.Information.Name));
						}

						if (invalid)
						{
							if (invalidTeamMembersList == null)
							{
								invalidTeamMembersList = new List<ITeamMember>();
							}
							teamMember.AssignedWorkItems = null;
							invalidTeamMembersList.Add(teamMember);
						}
					}
					if (invalidTeamMembersList != null)
					{
						invalidTeamMembersList.ForEach(x => TeamMembers.Remove(x));
					}

					// Update any remaining team members in the current list
					Parallel.ForEach(TeamMembers, teamMember => UpdateAssignedWorkItems(teamMember, sprintBacklogItems));

					break;
				}
			}
		}

		private static string GetNormalizedTeamName(string teamName)
		{
			if (teamName == null)
			{
				return string.Empty;
			}

			return teamName;
		}

		private static void UpdateAssignedWorkItems(ITeamMember teamMember, IEnumerable<IWorkItemAdapter> sprintBacklogItems)
		{
			teamMember.AssignedWorkItems = teamMember.Information.Unassigned
			                               	? GetUnassignedBacklogItemsForTeam(teamMember.Information.Team, sprintBacklogItems)
			                               	: GetTeamMemberBacklogItems(teamMember.Information.Name, sprintBacklogItems);
		}

		private ITeamMember CreateUnassignedTeamMemberForTeam(string teamName, IEnumerable<IWorkItemAdapter> workItems)
		{
			string key = string.Format("unassigned:{0}", teamName);
			var teamMemberInformation = TeamInfoProvider.GetTeamMemberInformation(key);

			if (teamMemberInformation == null)
			{
				throw new TeamMemberInformationNotFoundException(key);
			}

			var parameters = new Dictionary<string, object>
			                 	{
			                 		{"teamMemberInfo", teamMemberInformation},
			                 		{
			                 			"assignedWorkItems",
			                 			(workItems != null ? GetUnassignedBacklogItemsForTeam(teamName, workItems) : EmptyWorkItemsList)
			                 			}
			                 	};
			var teamMember = this.kernel.Get<ITeamMember>(With.Parameters.ConstructorArguments(parameters));

			return teamMember;
		}

		private ITeamMember CreateTeamMember(string teamMemberName, IEnumerable<IWorkItemAdapter> workItems)
		{
			Debug.Assert(!string.IsNullOrEmpty(teamMemberName));

			var teamMemberInformation = TeamInfoProvider.GetTeamMemberInformation(teamMemberName);

			if (teamMemberInformation == null)
			{
				throw new TeamMemberInformationNotFoundException(teamMemberName);
			}

			var parameters = new Dictionary<string, object>
			                 	{
			                 		{"teamMemberInfo", teamMemberInformation},
			                 		{
			                 			"assignedWorkItems",
			                 			(workItems != null ? GetTeamMemberBacklogItems(teamMemberName, workItems) : EmptyWorkItemsList)
			                 			}
			                 	};
			var teamMember = this.kernel.Get<ITeamMember>(With.Parameters.ConstructorArguments(parameters));

			return teamMember;
		}

		private static IEnumerable<IWorkItemAdapter> GetTeamMemberBacklogItems(string teamMemberName, IEnumerable<IWorkItemAdapter> sprintBacklogItems)
		{
			bool isWorkItemUnassigned = string.IsNullOrEmpty(teamMemberName);

			return sprintBacklogItems.Where(x => isWorkItemUnassigned ? string.IsNullOrEmpty(x.AssignedTo) : x.AssignedTo == teamMemberName);
		}

		private static IEnumerable<IWorkItemAdapter> GetUnassignedBacklogItemsForTeam(string teamName, IEnumerable<IWorkItemAdapter> sprintBacklogItems)
		{
			return sprintBacklogItems.Where(x => string.IsNullOrEmpty(x.AssignedTo) && GetNormalizedTeamName(x.Team) == GetNormalizedTeamName(teamName));
		}

		private double GetTotalHoursForTeamMember(ITeamMember teamMember)
		{
			double teamMemberTotalTime;

			try
			{
				teamMemberTotalTime = GetTeamMemberTotalTime(teamMember, this.availableTimeCalculator);
			}
			catch (ConnectionException ce)
			{
				this.logger.Warn(ce, "Exception detected while calculating total hours. Reverting to simple calculation for team member {0}.",
				                 teamMember.DisplayName);

				teamMemberTotalTime = GetTeamMemberTotalTime(teamMember, this.simpleAvailableTimeCalculator);
			}

			return teamMemberTotalTime * teamMember.Information.HourlyProductivityFactor;
		}

		private void CalculateAvailableTimeForTeamMember(ITeamMember teamMember)
		{
			double teamMemberAvailableHours;

			try
			{
				teamMemberAvailableHours = GetTeamMemberAvailableHours(teamMember, this.availableTimeCalculator);
			}
			catch (ConnectionException ce)
			{
				this.logger.Warn(ce, "Exception detected while calculating available hours. Reverting to simple calculation for team member {0}.",
				                 teamMember.DisplayName);

				teamMemberAvailableHours = GetTeamMemberAvailableHours(teamMember, this.simpleAvailableTimeCalculator);
			}

			teamMember.RemainingWorkload.PotentialSprintHours = teamMemberAvailableHours * teamMember.Information.HourlyProductivityFactor;
		}

		private double GetTeamMemberAvailableHours(ITeamMember teamMember, ITeamMemberAvailableTimeCalculator timeCalculator)
		{
			var startTimeStamp = SystemTime.Now();

			// Make sure that the start time is not previous to the sprint start
			if (startTimeStamp < Start)
			{
				startTimeStamp = Start;
			}

			double sprintHours = timeCalculator.GetAvailableHours(teamMember.Information, startTimeStamp, End, TimeCalculationOptions.FutureOnly);
			teamMember.RemainingWorkload.IncludesFreeBusyInformation = timeCalculator.HasFreeBusyInformationFor(teamMember.Information);
			return sprintHours;
		}

		private double GetTeamMemberTotalTime(ITeamMember teamMember, ITeamMemberAvailableTimeCalculator timeCalculator)
		{
			return timeCalculator.GetAvailableHours(teamMember.Information, Start, End, TimeCalculationOptions.PastAndFuture);
		}

		private void OnTimer(object state)
		{
			if (this.syncContext != null)
			{
				this.syncContext.Post(RefreshData, null);
			}
		}

		private void RefreshData(object state)
		{
			if (this.disposed)
			{
				return;
			}

			try
			{
				RefreshData(true, false);
			}
			finally
			{
				if (this.timer != null && !this.disposed)
				{
					this.timer.Change(60000, Timeout.Infinite);
				}
			}
		}

		private void RefreshData(bool refreshWorkItems, bool forceRefresh)
		{
			try
			{
				GuardDisposed();

				if (LastRefresh.HasValue && LastUpdate.HasValue)
				{
					if ((LastRefresh.Value - LastUpdate.Value) >= this.workItemQueryLifetime)
					{
						// If no work items have been updated in the last query, at least update
						// the available time calculations
						UpdateTimeCalculations();
					}
				}

				if (forceRefresh || !refreshWorkItems || WorkItemQueryOutdated)
				{
					this.logger.Info("Requesting work item store refresh...");

					try
					{
						if (!this.connection.IsConnected)
						{
							this.connection.Connect();

							if (this.disposed)
							{
								return;
							}
						}

						if (forceRefresh || refreshWorkItems || this.workItemStore == null)
						{
							bool initialGet = (WorkItemStore == null);

							if (initialGet)
							{
								WorkItemStore = this.connection.GetWorkItemStore(this.sprintConfiguration);
							}

							if (WorkItemStore != null)
							{
								if (refreshWorkItems)
								{
									WorkItemStore.Refresh();

									if (this.disposed) // WorkItemStore.Refresh may run its own message pump. Check if we have been disposed.
									{
										return;
									}
								}
								else
								{
									ProcessWorkItems();
								}
							}
						}
					}
					catch (ConnectionException ce)
					{
						this.logger.Error(ce, "Exception detected while requesting refresh of sprint work items.");

						Exception = ce;
					}
					catch (ArgumentException e)
					{
						this.logger.Error(e, "Exception detected while requesting refresh of sprint work items.");

						Exception = e;
					}
					catch (Exception e)
					{
						this.logger.Error(e, "Exception detected while requesting refresh of sprint work items.");

						Exception = e;
					}
				}
			}
			catch (Exception e)
			{
				this.logger.Error(e, "Unexpected exception detected in RefreshData.");

				throw;
			}

			this.logger.Info("Work item store refresh requested.");
		}

		#endregion

		#region Implementation of ISprint

		private ITeamMemberInfoProvider TeamInfoProvider
		{
			get
			{
				if (!this.teamInfoProviderSubscribed)
				{
					this.teamInfoProvider.PropertyChanged += OnTeamInfoProviderPropertyChanged;
					this.teamInfoProviderSubscribed = true;
				}

				return this.teamInfoProvider;
			}
		}

		public SprintStatus Status
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.status;
			}
			private set
			{
				GuardDisposed();

				if (value != this.status)
				{
					this.status = value;
					AsyncRaisePropertyChanged("Status");
				}
			}
		}

		public Exception Exception
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.exception;
			}
			private set
			{
				GuardDisposed();

				if (value != this.exception)
				{
					this.exception = value;

					this.logger.Info(value, "Exception assigned to ISprint.Exception.");

					AsyncRaisePropertyChanged("Exception");
				}
			}
		}

		/// <summary>
		/// Name of the sprint
		/// </summary>
		public string Name
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				if (!this.connection.IsConnected)
				{
					this.connection.Connect();
				}

				this.name = this.connection.GetSprintName(this.sprintConfiguration);

				return this.name;
			}
		}

		/// <summary>
		/// Timestamp of the start of the sprint
		/// </summary>
		public DateTime Start
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.sprintConfiguration.GetLocalSprintStartDate();
			}
		}

		/// <summary>
		/// Timestamp of the scheduled end of the sprint
		/// </summary>
		public DateTime End
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.sprintConfiguration.GetLocalSprintEndDate();
			}
		}

		public IWorkItemStore WorkItemStore
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.workItemStore;
			}
			private set
			{
				GuardDisposed();

				if (value != this.workItemStore)
				{
					if (this.workItemStore != null)
					{
						this.workItemStore.WorkItemsUpdated -= OnWorkItemsUpdated;
						this.workItemStore.WorkItemsRefreshed -= OnWorkItemsRefreshed;
					}

					this.workItemStore = value;
					AsyncRaisePropertyChanged("WorkItemStore");

					if (this.workItemStore != null)
					{
						this.workItemStore.WorkItemsUpdated += OnWorkItemsUpdated;
						this.workItemStore.WorkItemsRefreshed += OnWorkItemsRefreshed;
					}
				}
			}
		}

		/// <summary>
		/// Sum of the man-hours available to complete this sprint
		/// </summary>
		public TimeSpan AvailableTime
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.availableTime;
			}
			private set
			{
				GuardDisposed();

				if (value != this.availableTime)
				{
					this.availableTime = value;
					AsyncRaisePropertyChanged("AvailableTime");
				}
			}
		}

		public TimeSpan TotalTime
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.totalTime;
			}
			private set
			{
				GuardDisposed();

				if (value != this.totalTime)
				{
					this.totalTime = value;
					AsyncRaisePropertyChanged("TotalTime");
				}
			}
		}

		public bool TimeIncludesFreeBusyInformation
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.timeIncludesFreeBusyInformation;
			}
			private set
			{
				GuardDisposed();

				if (value != this.timeIncludesFreeBusyInformation)
				{
					this.timeIncludesFreeBusyInformation = value;
					AsyncRaisePropertyChanged("TimeIncludesFreeBusyInformation");
				}
			}
		}

		public bool NotAllWorkItemsEstimated
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.notAllWorkItemsEstimated;
			}
			private set
			{
				GuardDisposed();

				if (value != this.notAllWorkItemsEstimated)
				{
					this.notAllWorkItemsEstimated = value;
					AsyncRaisePropertyChanged("NotAllWorkItemsEstimated");
				}
			}
		}

		/// <summary>
		/// Team members with sprint backlog items assigned to them
		/// </summary>
		public BindingList<ITeamMember> TeamMembers
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.teamMembersList;
			}
		}

		public ISprintBurndown Burndown
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.sprintBurndown;
			}
		}

		public void Refresh(bool forceRefresh)
		{
			RefreshData(true, forceRefresh);
		}

		/// <summary>
		/// Timestamp of the last successful update
		/// </summary>
		public DateTime? LastRefresh
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.lastWorkItemsRefresh;
			}
			private set
			{
				GuardDisposed();

				if (value != this.lastWorkItemsRefresh)
				{
					this.lastWorkItemsRefresh = value;
					AsyncRaisePropertyChanged("LastRefresh");
				}
			}
		}

		public DateTime? LastUpdate
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.lastWorkItemsUpdate;
			}
			private set
			{
				GuardDisposed();

				if (value != this.lastWorkItemsUpdate)
				{
					this.lastWorkItemsUpdate = value;
					AsyncRaisePropertyChanged("LastUpdate");
				}
			}
		}

		#endregion

		#region Event Handlers

		private void OnWorkItemsRefreshed(object sender, RefreshResultsEventArgs e)
		{
			this.logger.Info("Work item store refreshed.");

			LastRefresh = SystemTime.Now();

			if (e.Exception != null)
			{
				this.logger.Info(e.Exception, "Exception found during a WorkItemStore request.");
			}

			Exception = e.Exception;
		}

		private void OnWorkItemsUpdated(object sender, EventArgs e)
		{
			this.logger.Info("Found updates in the work item store.");

			Debug.Assert(Exception == null);

			ProcessWorkItems();
		}

		private void OnTeamInfoProviderPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			lock (TeamMembers)
			{
				TeamMembers.Clear();
			}

			RefreshData(false, true);
		}

		#endregion

		#region Implementation of INotifyPropertyChanged

		public event PropertyChangedEventHandler PropertyChanged;

		private void RaisePropertyChanged(object state)
		{
			if (PropertyChanged != null)
			{
				string propertyName = (string) state;
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}

		private void AsyncRaisePropertyChanged(string propertyName)
		{
			if (this.syncContext == null || !Thread.CurrentThread.IsBackground)
			{
				RaisePropertyChanged(propertyName);
			}
			else
			{
				this.syncContext.Post(RaisePropertyChanged, propertyName);
			}
		}

		#endregion

		#region Implementation of IDisposable

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		private void Dispose(bool disposing)
		{
			if (disposing && !this.disposed)
			{
				if (this.timer != null)
				{
					this.timer.Change(Timeout.Infinite, Timeout.Infinite);
					this.timer.Dispose();
				}

				var disposable = this.sprintBurndown as IDisposable;
				if (disposable != null)
				{
					disposable.Dispose();
				}

				WorkItemStore = null;

				disposable = this.availableTimeCalculator as IDisposable;
				if (disposable != null)
				{
					disposable.Dispose();
				}

				if (this.teamInfoProviderSubscribed)
				{
					this.teamInfoProvider.PropertyChanged -= OnTeamInfoProviderPropertyChanged;
					this.teamInfoProviderSubscribed = false;
				}

				this.disposed = true;

				this.logger.Info("Sprint object has been disposed.");
			}
		}

		#endregion
	}
}